/*
 *  Copyright 2010 Susanta Tewari.
 *  This file is part of genomemap.
 *
 *  genomemap is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  genomemap is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with genomemap.  If not, see <http://www.gnu.org/licenses/>.
 */
package genomemap.map;

import genomemap.util.Util;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;

import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.swing.JPanel;

import org.apache.commons.lang3.ArrayUtils;

/**
 * 
 *
 * @version 1.0 2006
 * @author Susanta Tewari
 */
public class AlignmentMap extends JPanel {

    private int MAP_HEADER_VERT_INDENT = 40;

    private int MAP_HEADER_HORI_INDENT = 40;

    private int INTER_LABEL_VERT_GAP = 5;

    private int LABEL_LINE_GAP = 5;

    private int INTER_MAP_GAP = 150;

    private Font textFont = new Font("Courier New", Font.BOLD, 12);

    private Color lineColor = Color.RED;

    private BasicStroke lineStroke = new BasicStroke(2.5f,
            BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);

    private int LABEL_HEIGHT;

    private int mapWIDTH = 0;

    private int mapHEIGHT = 0;

    private int[][] MAP_STR_LEN = null;

    private int[] MAP_X = null;

    private int mapTotal = 0;

    private String[] mapHeaders = null;

    /**
     * anyCross has length = mapTotal - 1. boolean values indicate if
     * any lines should be drawn between the maps. provide null to
     * ignore crossing. //TODO rather than between consecutive maps,
     * provision should be there between any 2 maps
     */
    private boolean[] anyCross = null;

    private int[] crossCounts = null;

    private String[][] labels = null;

    public AlignmentMap() {
    }

    /**
     *
     * @param maps
     * @param useCrosses
     * @throws MapException if the common data is empty
     */
    public AlignmentMap(List<? extends Map> maps, boolean useCrosses) throws MapException {

        mapTotal = maps.size();
        labels = new String[maps.size()][];
        mapHeaders = new String[maps.size()];

        Set<String> commonSet = new HashSet<String>(maps.get(0).getLabels());
        for (int i = 0; i < mapTotal; i++) {
            commonSet.retainAll(maps.get(i).getLabels());
        }

        if (commonSet.size() == 0) throw new MapException("common set is empty");

        for (int i = 0; i < mapTotal; i++) {

            LinkedHashSet originalSet = new LinkedHashSet(maps.get(i).getLabels());
            originalSet.retainAll(commonSet);

            this.labels[i] = (String[]) originalSet.toArray(new String[0]);
            mapHeaders[i] = maps.get(i).getTitle();

        }

        anyCross = new boolean[labels.length];
        Arrays.fill(anyCross, useCrosses);

        instantiate();
    }

    /**
     * count of crossing will be measured only for "common" maps
     */
    private void instantiate() {


        LABEL_HEIGHT = getFontMetrics(textFont).getHeight();

        MAP_HEADER_HORI_INDENT = Util.findMax(pixelWidth(mapHeaders,
                this.textFont)) / 2;

        MAP_STR_LEN = new int[mapTotal][];

        MAP_X = new int[mapTotal];

        for (int i = 0; i < mapTotal; i++) {

            MAP_STR_LEN[i] = pixelWidth(labels[i], textFont);

            if (i == 0) {

                MAP_X[0] = MAP_HEADER_HORI_INDENT;

            } else {

                MAP_X[i] = MAP_X[i - 1] + Util.findMax(this.MAP_STR_LEN[i - 1]) + INTER_MAP_GAP;
            }

            mapWIDTH += Util.findMax(MAP_STR_LEN[i]);

        }

        mapHEIGHT = 2 * MAP_HEADER_VERT_INDENT + (labels[0].length - 1) * INTER_LABEL_VERT_GAP + labels[0].length
                * LABEL_HEIGHT;

        mapWIDTH += 2 * MAP_HEADER_HORI_INDENT + (mapTotal - 1) * INTER_MAP_GAP;

        setPreferredSize(new Dimension(mapWIDTH, mapHEIGHT));
        setBackground(Color.white);

    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);

        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setStroke(lineStroke);

        // create background
        g2.setColor(Color.white);
        g2.fillRect(0, 0, mapWIDTH, mapHEIGHT);

        // set up text color and font and rendering hints
        g2.setColor(Color.black);
        g2.setFont(textFont);

        for (int i = 0; i < mapTotal; i++) {

            // draw map header
            g2.drawString(mapHeaders[i], MAP_X[i] + pixelWidth(labels[i][0], textFont) / 2 - pixelWidth(
                    mapHeaders[i], textFont) / 2,
                    MAP_HEADER_VERT_INDENT / 2);

            // draw map
            for (int labelIndex = 0; labelIndex < labels[i].length; labelIndex++) {

                g2.drawString(labels[i][labelIndex], MAP_X[i],
                        MAP_HEADER_VERT_INDENT + labelIndex * (LABEL_HEIGHT + INTER_LABEL_VERT_GAP));
            }
        }

        // draw lines if applicable : from (i-1) map -> i map
        if (anyCross != null) {

            g2.setStroke(lineStroke);
            g2.setColor(lineColor);

            int crossMapIndex = 0;
            for (int i = 1; i < mapTotal; i++) {

                if (anyCross[crossMapIndex]) {

                    for (int labelIndex = 0; labelIndex < labels[i - 1].length; labelIndex++) {

                        int index = ArrayUtils.indexOf(labels[i],
                                labels[i - 1][labelIndex]);

                        if (index < 0) {
                            throw new RuntimeException("line: map" + (i - 1) + " -> map" + i
                                    + ". label: " + labels[i - 1][labelIndex] + " not found");
                        }

                        g2.drawLine(
                                MAP_X[i - 1] + MAP_STR_LEN[i - 1][labelIndex] + LABEL_LINE_GAP,
                                MAP_HEADER_VERT_INDENT + labelIndex * (LABEL_HEIGHT
                                + INTER_LABEL_VERT_GAP) - 2,
                                MAP_X[i] - LABEL_LINE_GAP,
                                MAP_HEADER_VERT_INDENT + index * (LABEL_HEIGHT
                                + INTER_LABEL_VERT_GAP) - 2);

                    }
                    crossMapIndex++;
                }
            }

        }

    }

    @Override
    public int getHeight() {
        return mapHEIGHT;
    }

    @Override
    public int getWidth() {
        return mapWIDTH;
    }

    private int[] pixelWidth(String[] words, Font font) {
        int[] temp = new int[words.length];
        for (int i = 0; i < words.length; i++) {
            temp[i] = pixelWidth(words[i], font);
        }
        return temp;
    }

    private int pixelWidth(String word, Font font) {
        return (this.getFontMetrics(font).stringWidth(word));
    }

    public List<Integer> getCrossCounts() {

        if (crossCounts == null) {

            crossCounts = new int[anyCross.length];

            if (anyCross != null) {

                int crossMapIndex = 0;
                for (int i = 1; i < mapTotal; i++) {

                    if (anyCross[crossMapIndex]) {

                        for (int labelIndex = 0; labelIndex < labels[i - 1].length; labelIndex++) {

                            int index = ArrayUtils.indexOf(labels[i],
                                    labels[i - 1][labelIndex]);

                            if (index < 0) {
                                throw new RuntimeException("line: map" + (i - 1) + " -> map" + i
                                        + ". label: " + labels[i - 1][labelIndex] + " not found");
                            }

                            if (index != labelIndex) {
                                crossCounts[crossMapIndex]++;
                            }

                        }
                        crossMapIndex++;
                    }
                }

            }

        }

        return Arrays.asList(ArrayUtils.toObject(crossCounts));
    }

}
